package jvs.vfs.tools;

/*
 * Copyright (c) 2003 Sun Microsystems, Inc. All  Rights Reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 
 * -Redistributions of source code must retain the above copyright
 *  notice, this list of conditions and the following disclaimer.
 * 
 * -Redistribution in binary form must reproduct the above copyright
 *  notice, this list of conditions and the following disclaimer in
 *  the documentation and/or other materials provided with the distribution.
 * 
 * Neither the name of Sun Microsystems, Inc. or the names of contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 * 
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
 * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
 * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT
 * BE LIABLE FOR ANY DAMAGES OR LIABILITIES SUFFERED BY LICENSEE AS A RESULT
 * OF OR RELATING TO USE, MODIFICATION OR DISTRIBUTION OF THE SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
 * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
 * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
 * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE SOFTWARE, EVEN
 * IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 * 
 * You acknowledge that Software is not designed, licensed or intended for
 * use in the design, construction, operation or maintenance of any nuclear
 * facility.
 */

/*
 * @(#)Notepad.java	1.21 03/01/23
 */

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.JViewport;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.PlainDocument;
import javax.swing.text.StyleConstants;
import javax.swing.text.TextAction;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;

import x.java.awt.FileDialog;
import x.java.io.File;
import x.java.io.FileOutputStream;
import x.java.io.FileReader;
import x.java.lang.System;

/**
 * Sample application using the simple text editor component that supports only
 * one font.
 * 
 * @author Timothy Prinzing
 * @version 1.21 01/23/03
 */
public class Notepad extends JPanel {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	private static ResourceBundle resources;

	static {
		try {
			resources = ResourceBundle.getBundle(
					"jvs/vfs/tools/resources/Notepad", Locale.getDefault());
		} catch (MissingResourceException mre) {
			System.err().println("Notepad.properties not found");
			exit(1);
		}
	}

	public Notepad() {
		this(null);
	}

	public Notepad(String path) {
		super(true);
		setFocusable(true);
		// Force SwingSet to come up in the Cross Platform L&F
		//try {
			//UIManager.setLookAndFeel(UIManager
			//		.getCrossPlatformLookAndFeelClassName());
			// If you want the System L&F instead, comment out the above line
			// and
			// uncomment the following:
			//UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		//} catch (Exception exc) {
		//	System.err().println("Error loading L&F: " + exc);
		//}

		setBorder(BorderFactory.createEtchedBorder());
		setLayout(new BorderLayout());

		// create the embedded JTextComponent
		editor = createEditor();
		// Add this as a listener for undoable edits.
		editor.getDocument().addUndoableEditListener(undoHandler);

		// install the command table
		commands = new Hashtable();
		Action[] actions = getActions();
		for (int i = 0; i < actions.length; i++) {
			Action a = actions[i];
			// commands.put(a.getText(Action.NAME), a);
			commands.put(a.getValue(Action.NAME), a);
		}

		JScrollPane scroller = new JScrollPane();
		JViewport port = scroller.getViewport();
		port.add(editor);
		try {
			String vpFlag = resources.getString("ViewportBackingStore");
			Boolean bs = new Boolean(vpFlag);
			port.setBackingStoreEnabled(bs.booleanValue());
		} catch (MissingResourceException mre) {
			// just use the viewport default
		}

		menuItems = new Hashtable();
		menubar = createMenubar();
		add("North", menubar);
		JPanel panel = new JPanel();
		panel.setLayout(new BorderLayout());
		panel.add("North", createToolbar());
		panel.add("Center", scroller);
		add("Center", panel);
		add("South", createStatusbar());
		//
		loadFile(path);
	}

	private void loadFile(String path) {
		if (path == null) {
			return;
		}
		File f = new File(path);
		if (f.exists()) {
			Document oldDoc = getEditor().getDocument();
			if (oldDoc != null)
				oldDoc.removeUndoableEditListener(undoHandler);
			if (elementTreePanel != null) {
				elementTreePanel.setEditor(null);
			}
			getEditor().setDocument(new PlainDocument());
			setTitle(path);
			Thread loader = new FileLoader(f, editor.getDocument());
			loader.start();
		}
	}
	
	/**
	 * @param path
	 */
	private void setTitle(String title) {
		Component c = getFrame();
		if (c != null) {
			if (c instanceof Frame) {
				((Frame)c).setTitle(title);
			} else
			if (c instanceof JInternalFrame) {
				((JInternalFrame)c).setTitle(title);
			}
		}
	}

	private static void exit(int s) {
		System.exit(s);
	}
	
	public static void main(String[] args) {
		try {
			 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
			// UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());

			JFrame frame = new JFrame();
			frame.setTitle(resources.getString("Title"));
			frame.setBackground(Color.lightGray);
			frame.getContentPane().setLayout(new BorderLayout());
			frame.getContentPane().add("Center", args.length > 0 ? new Notepad(args[0]):new Notepad());
			frame.addWindowListener(new AppCloser());
			frame.pack();
			frame.setSize(500, 600);
			Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
			frame.setLocation((d.width - frame.getSize().width) / 2,
					(d.height - frame.getSize().height) / 2);
			frame.setVisible(true);
		} catch (Throwable t) {
			t.printStackTrace();
		}
	}

	/**
	 * Fetch the list of actions supported by this editor. It is implemented to
	 * return the list of actions supported by the embedded JTextComponent
	 * augmented with the actions defined locally.
	 */
	public Action[] getActions() {
		return TextAction.augmentList(editor.getActions(), defaultActions);
	}

	/**
	 * Create an editor to represent the given document.
	 */
	protected JTextComponent createEditor() {
		JTextComponent c = new JTextArea();
		c.setDragEnabled(true);
		c.setFont(new Font("monospaced", Font.PLAIN, 12));
		return c;
	}

	/**
	 * Fetch the editor contained in this panel
	 */
	protected JTextComponent getEditor() {
		return editor;
	}

	/**
	 * To shutdown when run as an application. This is a fairly lame
	 * implementation. A more self-respecting implementation would at least
	 * check to see if a save was needed.
	 */
	protected static final class AppCloser extends WindowAdapter {
		public void windowClosing(WindowEvent e) {
			exit(0);
		}
	}

	/**
	 * Find the hosting frame, for the file-chooser dialog.
	 */
	private Component getFrame() {
		for (Container p = getParent(); p != null; p = p.getParent()) {
			if (p instanceof JInternalFrame || p instanceof JFrame || p instanceof Frame) {
				return  p;
			}
		}
		return null;
	}

	/**
	 * This is the hook through which all menu items are created. It registers
	 * the result with the menuitem hashtable so that it can be fetched with
	 * getMenuItem().
	 * 
	 * @see #getMenuItem
	 */
	protected JMenuItem createMenuItem(String cmd) {
		JMenuItem mi = new JMenuItem(getResourceString(cmd + labelSuffix));
		URL url = getResource(cmd + imageSuffix);
		if (url != null) {
			mi.setHorizontalTextPosition(JButton.RIGHT);
			mi.setIcon(new ImageIcon(url));
		}
		String astr = getResourceString(cmd + actionSuffix);
		if (astr == null) {
			astr = cmd;
		}
		mi.setActionCommand(astr);
		Action a = getAction(astr);
		if (a != null) {
			mi.addActionListener(a);
			a.addPropertyChangeListener(createActionChangeListener(mi));
			mi.setEnabled(a.isEnabled());
		} else {
			mi.setEnabled(false);
		}
		menuItems.put(cmd, mi);
		return mi;
	}

	/**
	 * Fetch the menu item that was created for the given command.
	 * 
	 * @param cmd
	 *            Name of the action.
	 * @returns item created for the given command or null if one wasn't
	 *          created.
	 */
	protected JMenuItem getMenuItem(String cmd) {
		return (JMenuItem) menuItems.get(cmd);
	}

	protected Action getAction(String cmd) {
		return (Action) commands.get(cmd);
	}

	protected String getResourceString(String nm) {
		String str;
		try {
			str = resources.getString(nm);
		} catch (MissingResourceException mre) {
			str = null;
		}
		return str;
	}

	protected URL getResource(String key) {
		String name = getResourceString(key);
		if (name != null) {
			URL url = this.getClass().getResource(name);
			return url;
		}
		return null;
	}

	protected Container getToolbar() {
		return toolbar;
	}

	protected JMenuBar getMenubar() {
		return menubar;
	}

	/**
	 * Create a status bar
	 */
	protected Component createStatusbar() {
		// need to do something reasonable here
		status = new StatusBar();
		return status;
	}

	/**
	 * Resets the undo manager.
	 */
	protected void resetUndoManager() {
		undo.discardAllEdits();
		undoAction.update();
		redoAction.update();
	}

	/**
	 * Create the toolbar. By default this reads the resource file for the
	 * definition of the toolbar.
	 */
	private Component createToolbar() {
		toolbar = new JToolBar();
		String[] toolKeys = tokenize(getResourceString("toolbar"));
		for (int i = 0; i < toolKeys.length; i++) {
			if (toolKeys[i].equals("-")) {
				toolbar.add(Box.createHorizontalStrut(5));
			} else {
				toolbar.add(createTool(toolKeys[i]));
			}
		}
		toolbar.add(Box.createHorizontalGlue());
		return toolbar;
	}

	/**
	 * Hook through which every toolbar item is created.
	 */
	protected Component createTool(String key) {
		return createToolbarButton(key);
	}

	/**
	 * Create a button to go inside of the toolbar. By default this will load an
	 * image resource. The image filename is relative to the classpath
	 * (including the '.' directory if its a part of the classpath), and may
	 * either be in a JAR file or a separate file.
	 * 
	 * @param key
	 *            The key in the resource file to serve as the basis of lookups.
	 */
	protected JButton createToolbarButton(String key) {
		URL url = getResource(key + imageSuffix);
		JButton b = new JButton(new ImageIcon(url)) {
			public float getAlignmentY() {
				return 0.5f;
			}
		};
		b.setRequestFocusEnabled(false);
		b.setMargin(new Insets(1, 1, 1, 1));

		String astr = getResourceString(key + actionSuffix);
		if (astr == null) {
			astr = key;
		}
		Action a = getAction(astr);
		if (a != null) {
			b.setActionCommand(astr);
			b.addActionListener(a);
		} else {
			b.setEnabled(false);
		}

		String tip = getResourceString(key + tipSuffix);
		if (tip != null) {
			b.setToolTipText(tip);
		}

		return b;
	}

	/**
	 * Take the given string and chop it up into a series of strings on
	 * whitespace boundries. This is useful for trying to get an array of
	 * strings out of the resource file.
	 */
	protected String[] tokenize(String input) {
		Vector v = new Vector();
		StringTokenizer t = new StringTokenizer(input);
		String cmd[];

		while (t.hasMoreTokens())
			v.addElement(t.nextToken());
		cmd = new String[v.size()];
		for (int i = 0; i < cmd.length; i++)
			cmd[i] = (String) v.elementAt(i);

		return cmd;
	}

	/**
	 * Create the menubar for the app. By default this pulls the definition of
	 * the menu from the associated resource file.
	 */
	protected JMenuBar createMenubar() {
		JMenuItem mi;
		JMenuBar mb = new JMenuBar();

		String[] menuKeys = tokenize(getResourceString("menubar"));
		for (int i = 0; i < menuKeys.length; i++) {
			JMenu m = createMenu(menuKeys[i]);
			if (m != null) {
				mb.add(m);
			}
		}
		return mb;
	}

	/**
	 * Create a menu for the app. By default this pulls the definition of the
	 * menu from the associated resource file.
	 */
	protected JMenu createMenu(String key) {
		String[] itemKeys = tokenize(getResourceString(key));
		JMenu menu = new JMenu(getResourceString(key + "Label"));
		for (int i = 0; i < itemKeys.length; i++) {
			if (itemKeys[i].equals("-")) {
				menu.addSeparator();
			} else {
				JMenuItem mi = createMenuItem(itemKeys[i]);
				menu.add(mi);
			}
		}
		return menu;
	}

	// Yarked from JMenu, ideally this would be public.
	protected PropertyChangeListener createActionChangeListener(JMenuItem b) {
		return new ActionChangedListener(b);
	}

	// Yarked from JMenu, ideally this would be public.
	private class ActionChangedListener implements PropertyChangeListener {
		JMenuItem menuItem;

		ActionChangedListener(JMenuItem mi) {
			super();
			this.menuItem = mi;
		}

		public void propertyChange(PropertyChangeEvent e) {
			String propertyName = e.getPropertyName();
			if (e.getPropertyName().equals(Action.NAME)) {
				String text = (String) e.getNewValue();
				menuItem.setText(text);
			} else if (propertyName.equals("enabled")) {
				Boolean enabledState = (Boolean) e.getNewValue();
				menuItem.setEnabled(enabledState.booleanValue());
			}
		}
	}

	private JTextComponent editor;

	private Hashtable commands;

	private Hashtable menuItems;

	private JMenuBar menubar;

	private JToolBar toolbar;

	private JComponent status;

	private JFrame elementTreeFrame;

	protected ElementTreePanel elementTreePanel;

	protected FileDialog fileDialog;

	/**
	 * Listener for the edits on the current document.
	 */
	protected UndoableEditListener undoHandler = new UndoHandler();

	/** UndoManager that we add edits to. */
	protected UndoManager undo = new UndoManager();

	/**
	 * Suffix applied to the key used in resource file lookups for an image.
	 */
	public static final String imageSuffix = "Image";

	/**
	 * Suffix applied to the key used in resource file lookups for a label.
	 */
	public static final String labelSuffix = "Label";

	/**
	 * Suffix applied to the key used in resource file lookups for an action.
	 */
	public static final String actionSuffix = "Action";

	/**
	 * Suffix applied to the key used in resource file lookups for tooltip text.
	 */
	public static final String tipSuffix = "Tooltip";

	public static final String openAction = "open";

	public static final String newAction = "new";

	public static final String saveAction = "save";

	public static final String exitAction = "exit";

	public static final String showElementTreeAction = "showElementTree";

	class UndoHandler implements UndoableEditListener {

		/**
		 * Messaged when the Document has created an edit, the edit is added to
		 * <code>undo</code>, an instance of UndoManager.
		 */
		public void undoableEditHappened(UndoableEditEvent e) {
			undo.addEdit(e.getEdit());
			undoAction.update();
			redoAction.update();
		}
	}

	/**
	 * FIXME - I'm not very useful yet
	 */
	class StatusBar extends JComponent {

		public StatusBar() {
			super();
			setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
		}

		public void paint(Graphics g) {
			super.paint(g);
		}

	}

	// --- action implementations -----------------------------------

	private UndoAction undoAction = new UndoAction();

	private RedoAction redoAction = new RedoAction();

	/**
	 * Actions defined by the Notepad class
	 */
	private Action[] defaultActions = { new NewAction(), new OpenAction(),
			new SaveAction(), new ExitAction(), new ShowElementTreeAction(),
			undoAction, redoAction };

	class UndoAction extends AbstractAction {
		public UndoAction() {
			super("Undo");
			setEnabled(false);
		}

		public void actionPerformed(ActionEvent e) {
			try {
				undo.undo();
			} catch (CannotUndoException ex) {
				System.out().println("Unable to undo: " + ex);
				ex.printStackTrace();
			}
			update();
			redoAction.update();
		}

		protected void update() {
			if (undo.canUndo()) {
				setEnabled(true);
				putValue(Action.NAME, undo.getUndoPresentationName());
			} else {
				setEnabled(false);
				putValue(Action.NAME, "Undo");
			}
		}
	}

	class RedoAction extends AbstractAction {
		
		public RedoAction() {
			super("Redo");
			setEnabled(false);
		}

		public void actionPerformed(ActionEvent e) {
			try {
				undo.redo();
			} catch (CannotRedoException ex) {
				System.out().println("Unable to redo: " + ex);
				ex.printStackTrace();
			}
			update();
			undoAction.update();
		}

		protected void update() {
			if (undo.canRedo()) {
				setEnabled(true);
				putValue(Action.NAME, undo.getRedoPresentationName());
			} else {
				setEnabled(false);
				putValue(Action.NAME, "Redo");
			}
		}
	}

	class OpenAction extends NewAction {

		OpenAction() {
			super(openAction);
		}

		public void actionPerformed(ActionEvent e) {
			Component frame = getFrame();
			if (fileDialog == null) {
				fileDialog = new FileDialog(frame);
			}
			fileDialog.setMode(FileDialog.LOAD);
			fileDialog.show();

			String file = fileDialog.getFile();
			if (file == null) {
				return;
			}

			String directory = fileDialog.getDirectory();
			File f = new File(directory, file);
			if (f.exists()) {
				Document oldDoc = getEditor().getDocument();
				if (oldDoc != null)
					oldDoc.removeUndoableEditListener(undoHandler);
				if (elementTreePanel != null) {
					elementTreePanel.setEditor(null);
				}
				getEditor().setDocument(new PlainDocument());
				setTitle(f.getPath());
				Thread loader = new FileLoader(f, editor.getDocument());
				loader.start();
			}
		}
	}

	/**
	 * Trys to write the document as a serialization.
	 */
	class SaveAction extends AbstractAction {

		SaveAction() {
			super(saveAction);
		}

		public void actionPerformed(ActionEvent e) {
			Component frame = getFrame();
			if (fileDialog == null) {
				fileDialog = new FileDialog(frame);
			}
			fileDialog.setMode(FileDialog.SAVE);
			fileDialog.show();
			String file = fileDialog.getFile();
			if (file == null) {
				return;
			}
			String directory = fileDialog.getDirectory();
			File f = new File(directory, file);
			try {
				Writer w = new OutputStreamWriter(new FileOutputStream(f));
				w.write(getEditor().getText());
				w.flush();
				w.close();
			} catch (IOException ex) {
				// should put in status panel
				ex.printStackTrace();
				//System.err().println("IOException: " + ex.getMessage());
			}
		}
	}

	class NewAction extends AbstractAction {

		NewAction() {
			super(newAction);
		}

		NewAction(String nm) {
			super(nm);
		}

		public void actionPerformed(ActionEvent e) {
			Document oldDoc = getEditor().getDocument();
			if (oldDoc != null)
				oldDoc.removeUndoableEditListener(undoHandler);
			getEditor().setDocument(new PlainDocument());
			getEditor().getDocument().addUndoableEditListener(undoHandler);
			resetUndoManager();
			revalidate();
		}
	}

	/**
	 * Really lame implementation of an exit command
	 */
	class ExitAction extends AbstractAction {

		private static final long serialVersionUID = 1L;

		ExitAction() {
			super(exitAction);
		}

		public void actionPerformed(ActionEvent e) {
			exit(0);
			
			Component c = getFrame();
			if (c instanceof JInternalFrame) {
				JInternalFrame jif = (JInternalFrame)c;
				jif.dispose();
			}
		}
	}

	/**
	 * Action that brings up a JFrame with a JTree showing the structure of the
	 * document.
	 */
	class ShowElementTreeAction extends AbstractAction {

		ShowElementTreeAction() {
			super(showElementTreeAction);
		}

		ShowElementTreeAction(String nm) {
			super(nm);
		}

		public void actionPerformed(ActionEvent e) {
			if (elementTreeFrame == null) {
				// Create a frame containing an instance of
				// ElementTreePanel.
				try {
					String title = resources.getString("ElementTreeFrameTitle");
					elementTreeFrame = new JFrame(title);
				} catch (MissingResourceException mre) {
					elementTreeFrame = new JFrame();
				}

				elementTreeFrame.addWindowListener(new WindowAdapter() {
					public void windowClosing(WindowEvent weeee) {
						elementTreeFrame.setVisible(false);
					}
				});
				Container fContentPane = elementTreeFrame.getContentPane();

				fContentPane.setLayout(new BorderLayout());
				elementTreePanel = new ElementTreePanel(getEditor());
				fContentPane.add(elementTreePanel);
				elementTreeFrame.pack();
			}
			elementTreeFrame.show();
		}
	}

	/**
	 * Thread to load a file into the text storage model
	 */
	class FileLoader extends Thread {

		FileLoader(File f, Document doc) {
			setPriority(4);
			this.f = f;
			this.doc = doc;
		}

		public void run() {
			try {
				// initialize the statusbar
				status.removeAll();
				JProgressBar progress = new JProgressBar();
				progress.setMinimum(0);
				progress.setMaximum((int) f.length());
				status.add(progress);
				status.revalidate();

				// try to start reading
				Reader in = new FileReader(f);
				char[] buff = new char[4096];
				int nch;
				while ((nch = in.read(buff, 0, buff.length)) != -1) {
					doc.insertString(doc.getLength(), new String(buff, 0, nch),
							null);
					progress.setValue(progress.getValue() + nch);
				}

				// we are done... get rid of progressbar
				doc.addUndoableEditListener(undoHandler);
				status.removeAll();
				status.revalidate();

				resetUndoManager();
			} catch (IOException e) {
				System.err().println(e.toString());
				e.printStackTrace();
			} catch (BadLocationException e) {
				System.err().println(e.getMessage());
				e.printStackTrace();
			}
			if (elementTreePanel != null) {
				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						elementTreePanel.setEditor(getEditor());
					}
				});
			}
		}

		Document doc;

		File f;
	}

}

/*
 * Copyright (c) 2003 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * -Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 * 
 * -Redistribution in binary form must reproduct the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * Neither the name of Sun Microsystems, Inc. or the names of contributors may
 * be used to endorse or promote products derived from this software without
 * specific prior written permission.
 * 
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
 * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR
 * NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE
 * LIABLE FOR ANY DAMAGES OR LIABILITIES SUFFERED BY LICENSEE AS A RESULT OF OR
 * RELATING TO USE, MODIFICATION OR DISTRIBUTION OF THE SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
 * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
 * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
 * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF
 * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 * 
 * You acknowledge that Software is not designed, licensed or intended for use
 * in the design, construction, operation or maintenance of any nuclear
 * facility.
 */

/*
 * @(#)ElementTreePanel.java 1.14 03/01/23
 */

/**
 * Displays a tree showing all the elements in a text Document. Selecting a node
 * will result in reseting the selection of the JTextComponent. This also
 * becomes a CaretListener to know when the selection has changed in the text to
 * update the selected item in the tree.
 * 
 * @author Scott Violet
 * @version 1.14 01/23/03
 */
class ElementTreePanel extends JPanel implements CaretListener,
		DocumentListener, PropertyChangeListener, TreeSelectionListener {
	/** Tree showing the documents element structure. */
	protected JTree tree;

	/** Text component showing elemenst for. */
	protected JTextComponent editor;

	/** Model for the tree. */
	protected ElementTreeModel treeModel;

	/** Set to true when updatin the selection. */
	protected boolean updatingSelection;

	public ElementTreePanel(JTextComponent editor) {
		this.editor = editor;

		Document document = editor.getDocument();

		// Create the tree.
		treeModel = new ElementTreeModel(document);
		tree = new JTree(treeModel) {
			public String convertValueToText(Object value, boolean selected,
					boolean expanded, boolean leaf, int row, boolean hasFocus) {
				// Should only happen for the root
				if (!(value instanceof Element))
					return value.toString();

				Element e = (Element) value;
				AttributeSet as = e.getAttributes().copyAttributes();
				String asString;

				if (as != null) {
					StringBuffer retBuffer = new StringBuffer("[");
					Enumeration names = as.getAttributeNames();

					while (names.hasMoreElements()) {
						Object nextName = names.nextElement();

						if (nextName != StyleConstants.ResolveAttribute) {
							retBuffer.append(" ");
							retBuffer.append(nextName);
							retBuffer.append("=");
							retBuffer.append(as.getAttribute(nextName));
						}
					}
					retBuffer.append(" ]");
					asString = retBuffer.toString();
				} else
					asString = "[ ]";

				if (e.isLeaf())
					return e.getName() + " [" + e.getStartOffset() + ", "
							+ e.getEndOffset() + "] Attributes: " + asString;
				return e.getName() + " [" + e.getStartOffset() + ", "
						+ e.getEndOffset() + "] Attributes: " + asString;
			}
		};
		tree.addTreeSelectionListener(this);
		tree.setDragEnabled(true);
		// Don't show the root, it is fake.
		tree.setRootVisible(false);
		// Since the display value of every node after the insertion point
		// changes every time the text changes and we don't generate a change
		// event for all those nodes the display value can become off.
		// This can be seen as '...' instead of the complete string value.
		// This is a temporary workaround, increase the needed size by 15,
		// hoping that will be enough.
		tree.setCellRenderer(new DefaultTreeCellRenderer() {
			public Dimension getPreferredSize() {
				Dimension retValue = super.getPreferredSize();
				if (retValue != null)
					retValue.width += 15;
				return retValue;
			}
		});
		// become a listener on the document to update the tree.
		document.addDocumentListener(this);

		// become a PropertyChangeListener to know when the Document has
		// changed.
		editor.addPropertyChangeListener(this);

		// Become a CaretListener
		editor.addCaretListener(this);

		// configure the panel and frame containing it.
		setLayout(new BorderLayout());
		add(new JScrollPane(tree), BorderLayout.CENTER);

		// Add a label above tree to describe what is being shown
		JLabel label = new JLabel("Elements that make up the current document",
				SwingConstants.CENTER);

		label.setFont(new Font("Dialog", Font.BOLD, 14));
		add(label, BorderLayout.NORTH);

		setPreferredSize(new Dimension(400, 400));
	}

	/**
	 * Resets the JTextComponent to <code>editor</code>. This will update the
	 * tree accordingly.
	 */
	public void setEditor(JTextComponent editor) {
		if (this.editor == editor) {
			return;
		}

		if (this.editor != null) {
			Document oldDoc = this.editor.getDocument();

			oldDoc.removeDocumentListener(this);
			this.editor.removePropertyChangeListener(this);
			this.editor.removeCaretListener(this);
		}
		this.editor = editor;
		if (editor == null) {
			treeModel = null;
			tree.setModel(null);
		} else {
			Document newDoc = editor.getDocument();

			newDoc.addDocumentListener(this);
			editor.addPropertyChangeListener(this);
			editor.addCaretListener(this);
			treeModel = new ElementTreeModel(newDoc);
			tree.setModel(treeModel);
		}
	}

	// PropertyChangeListener

	/**
	 * Invoked when a property changes. We are only interested in when the
	 * Document changes to reset the DocumentListener.
	 */
	public void propertyChange(PropertyChangeEvent e) {
		if (e.getSource() == getEditor()
				&& e.getPropertyName().equals("document")) {
			JTextComponent editor = getEditor();
			Document oldDoc = (Document) e.getOldValue();
			Document newDoc = (Document) e.getNewValue();

			// Reset the DocumentListener
			oldDoc.removeDocumentListener(this);
			newDoc.addDocumentListener(this);

			// Recreate the TreeModel.
			treeModel = new ElementTreeModel(newDoc);
			tree.setModel(treeModel);
		}
	}

	// DocumentListener

	/**
	 * Gives notification that there was an insert into the document. The given
	 * range bounds the freshly inserted region.
	 * 
	 * @param e
	 *            the document event
	 */
	public void insertUpdate(DocumentEvent e) {
		updateTree(e);
	}

	/**
	 * Gives notification that a portion of the document has been removed. The
	 * range is given in terms of what the view last saw (that is, before
	 * updating sticky positions).
	 * 
	 * @param e
	 *            the document event
	 */
	public void removeUpdate(DocumentEvent e) {
		updateTree(e);
	}

	/**
	 * Gives notification that an attribute or set of attributes changed.
	 * 
	 * @param e
	 *            the document event
	 */
	public void changedUpdate(DocumentEvent e) {
		updateTree(e);
	}

	// CaretListener

	/**
	 * Messaged when the selection in the editor has changed. Will update the
	 * selection in the tree.
	 */
	public void caretUpdate(CaretEvent e) {
		if (!updatingSelection) {
			JTextComponent editor = getEditor();
			int selBegin = Math.min(e.getDot(), e.getMark());
			int end = Math.max(e.getDot(), e.getMark());
			Vector paths = new Vector();
			TreeModel model = getTreeModel();
			Object root = model.getRoot();
			int rootCount = model.getChildCount(root);

			// Build an array of all the paths to all the character elements
			// in the selection.
			for (int counter = 0; counter < rootCount; counter++) {
				int start = selBegin;

				while (start <= end) {
					TreePath path = getPathForIndex(start, root,
							(Element) model.getChild(root, counter));
					Element charElement = (Element) path.getLastPathComponent();

					paths.addElement(path);
					if (start >= charElement.getEndOffset())
						start++;
					else
						start = charElement.getEndOffset();
				}
			}

			// If a path was found, select it (them).
			int numPaths = paths.size();

			if (numPaths > 0) {
				TreePath[] pathArray = new TreePath[numPaths];

				paths.copyInto(pathArray);
				updatingSelection = true;
				try {
					getTree().setSelectionPaths(pathArray);
					getTree().scrollPathToVisible(pathArray[0]);
				} finally {
					updatingSelection = false;
				}
			}
		}
	}

	// TreeSelectionListener

	/**
	 * Called whenever the value of the selection changes.
	 * 
	 * @param e
	 *            the event that characterizes the change.
	 */
	public void valueChanged(TreeSelectionEvent e) {
		JTree tree = getTree();

		if (!updatingSelection && tree.getSelectionCount() == 1) {
			TreePath selPath = tree.getSelectionPath();
			Object lastPathComponent = selPath.getLastPathComponent();

			if (!(lastPathComponent instanceof DefaultMutableTreeNode)) {
				Element selElement = (Element) lastPathComponent;

				updatingSelection = true;
				try {
					getEditor().select(selElement.getStartOffset(),
							selElement.getEndOffset());
				} finally {
					updatingSelection = false;
				}
			}
		}
	}

	// Local methods

	/**
	 * @return tree showing elements.
	 */
	protected JTree getTree() {
		return tree;
	}

	/**
	 * @return JTextComponent showing elements for.
	 */
	protected JTextComponent getEditor() {
		return editor;
	}

	/**
	 * @return TreeModel implementation used to represent the elements.
	 */
	public DefaultTreeModel getTreeModel() {
		return treeModel;
	}

	/**
	 * Updates the tree based on the event type. This will invoke either
	 * updateTree with the root element, or handleChange.
	 */
	protected void updateTree(DocumentEvent event) {
		updatingSelection = true;
		try {
			TreeModel model = getTreeModel();
			Object root = model.getRoot();

			for (int counter = model.getChildCount(root) - 1; counter >= 0; counter--) {
				updateTree(event, (Element) model.getChild(root, counter));
			}
		} finally {
			updatingSelection = false;
		}
	}

	/**
	 * Creates TreeModelEvents based on the DocumentEvent and messages the
	 * treemodel. This recursively invokes this method with children elements.
	 * 
	 * @param event
	 *            indicates what elements in the tree hierarchy have changed.
	 * @param element
	 *            Current element to check for changes against.
	 */
	protected void updateTree(DocumentEvent event, Element element) {
		DocumentEvent.ElementChange ec = event.getChange(element);

		if (ec != null) {
			Element[] removed = ec.getChildrenRemoved();
			Element[] added = ec.getChildrenAdded();
			int startIndex = ec.getIndex();

			// Check for removed.
			if (removed != null && removed.length > 0) {
				int[] indices = new int[removed.length];

				for (int counter = 0; counter < removed.length; counter++) {
					indices[counter] = startIndex + counter;
				}
				getTreeModel().nodesWereRemoved((TreeNode) element, indices,
						removed);
			}
			// check for added
			if (added != null && added.length > 0) {
				int[] indices = new int[added.length];

				for (int counter = 0; counter < added.length; counter++) {
					indices[counter] = startIndex + counter;
				}
				getTreeModel().nodesWereInserted((TreeNode) element, indices);
			}
		}
		if (!element.isLeaf()) {
			int startIndex = element.getElementIndex(event.getOffset());
			int elementCount = element.getElementCount();
			int endIndex = Math.min(elementCount - 1, element
					.getElementIndex(event.getOffset() + event.getLength()));

			if (startIndex > 0
					&& startIndex < elementCount
					&& element.getElement(startIndex).getStartOffset() == event
							.getOffset()) {
				// Force checking the previous element.
				startIndex--;
			}
			if (startIndex != -1 && endIndex != -1) {
				for (int counter = startIndex; counter <= endIndex; counter++) {
					updateTree(event, element.getElement(counter));
				}
			}
		} else {
			// Element is a leaf, assume it changed
			getTreeModel().nodeChanged((TreeNode) element);
		}
	}

	/**
	 * Returns a TreePath to the element at <code>position</code>.
	 */
	protected TreePath getPathForIndex(int position, Object root,
			Element rootElement) {
		TreePath path = new TreePath(root);
		Element child = rootElement.getElement(rootElement
				.getElementIndex(position));

		path = path.pathByAddingChild(rootElement);
		path = path.pathByAddingChild(child);
		while (!child.isLeaf()) {
			child = child.getElement(child.getElementIndex(position));
			path = path.pathByAddingChild(child);
		}
		return path;
	}

	/**
	 * ElementTreeModel is an implementation of TreeModel to handle displaying
	 * the Elements from a Document. AbstractDocument.AbstractElement is the
	 * default implementation used by the swing text package to implement
	 * Element, and it implements TreeNode. This makes it trivial to create a
	 * DefaultTreeModel rooted at a particular Element from the Document.
	 * Unfortunately each Document can have more than one root Element. Implying
	 * that to display all the root elements as a child of another root a fake
	 * node has be created. This class creates a fake node as the root with the
	 * children being the root elements of the Document (getRootElements).
	 * <p>
	 * This subclasses DefaultTreeModel. The majority of the TreeModel methods
	 * have been subclassed, primarily to special case the root.
	 */
	public static class ElementTreeModel extends DefaultTreeModel {
		protected Element[] rootElements;

		public ElementTreeModel(Document document) {
			super(new DefaultMutableTreeNode("root"), false);
			rootElements = document.getRootElements();
		}

		/**
		 * Returns the child of <I>parent</I> at index <I>index</I> in the
		 * parent's child array. <I>parent</I> must be a node previously
		 * obtained from this data source. This should not return null if
		 * <i>index</i> is a valid index for <i>parent</i> (that is <i>index</i> >=
		 * 0 && <i>index</i> < getChildCount(<i>parent</i>)).
		 * 
		 * @param parent
		 *            a node in the tree, obtained from this data source
		 * @return the child of <I>parent</I> at index <I>index</I>
		 */
		public Object getChild(Object parent, int index) {
			if (parent == root)
				return rootElements[index];
			return super.getChild(parent, index);
		}

		/**
		 * Returns the number of children of <I>parent</I>. Returns 0 if the
		 * node is a leaf or if it has no children. <I>parent</I> must be a
		 * node previously obtained from this data source.
		 * 
		 * @param parent
		 *            a node in the tree, obtained from this data source
		 * @return the number of children of the node <I>parent</I>
		 */
		public int getChildCount(Object parent) {
			if (parent == root)
				return rootElements.length;
			return super.getChildCount(parent);
		}

		/**
		 * Returns true if <I>node</I> is a leaf. It is possible for this
		 * method to return false even if <I>node</I> has no children. A
		 * directory in a filesystem, for example, may contain no files; the
		 * node representing the directory is not a leaf, but it also has no
		 * children.
		 * 
		 * @param node
		 *            a node in the tree, obtained from this data source
		 * @return true if <I>node</I> is a leaf
		 */
		public boolean isLeaf(Object node) {
			if (node == root)
				return false;
			return super.isLeaf(node);
		}

		/**
		 * Returns the index of child in parent.
		 */
		public int getIndexOfChild(Object parent, Object child) {
			if (parent == root) {
				for (int counter = rootElements.length - 1; counter >= 0; counter--) {
					if (rootElements[counter] == child)
						return counter;
				}
				return -1;
			}
			return super.getIndexOfChild(parent, child);
		}

		/**
		 * Invoke this method after you've changed how node is to be represented
		 * in the tree.
		 */
		public void nodeChanged(TreeNode node) {
			if (listenerList != null && node != null) {
				TreeNode parent = node.getParent();

				if (parent == null && node != root) {
					parent = root;
				}
				if (parent != null) {
					int anIndex = getIndexOfChild(parent, node);

					if (anIndex != -1) {
						int[] cIndexs = new int[1];

						cIndexs[0] = anIndex;
						nodesChanged(parent, cIndexs);
					}
				}
			}
		}

		/**
		 * Returns the path to a particluar node. This is recursive.
		 */
		protected TreeNode[] getPathToRoot(TreeNode aNode, int depth) {
			TreeNode[] retNodes;

			/*
			 * Check for null, in case someone passed in a null node, or they
			 * passed in an element that isn't rooted at root.
			 */
			if (aNode == null) {
				if (depth == 0)
					return null;
				else
					retNodes = new TreeNode[depth];
			} else {
				depth++;
				if (aNode == root)
					retNodes = new TreeNode[depth];
				else {
					TreeNode parent = aNode.getParent();

					if (parent == null)
						parent = root;
					retNodes = getPathToRoot(parent, depth);
				}
				retNodes[retNodes.length - depth] = aNode;
			}
			return retNodes;
		}
	}
}
